From 2e161b0e9f3b05bc0439d4fb90a908e3a4879ba1 Mon Sep 17 00:00:00 2001 From: Kirill Sizov Date: Mon, 20 Jun 2022 17:09:25 +0300 Subject: [PATCH] Refactor CI (#26) --- .github/workflows/cache.yml | 2 +- .github/workflows/main.yml | 217 +++++------- .github/workflows/schedule.yml | 131 +++++++- CHANGELOG.md | 2 + .../contributing/development-environment.md | 1 + .../en/docs/contributing/running-tests.md | 94 ++++++ ...ss_cron_type.json => nightly_cypress.json} | 0 tests/pr_cypress.json | 37 +++ tests/pr_cypress_canvas3d.json | 22 ++ tests/rest_api/README.md | 27 +- .../rest_api/assets/cvat_db/cvat_data.tar.bz2 | Bin 45841 -> 75638 bytes tests/rest_api/conftest.py | 308 +----------------- tests/rest_api/docker-compose.minio.yml | 8 +- tests/rest_api/fixtures/__init__.py | 3 + tests/rest_api/fixtures/data.py | 276 ++++++++++++++++ tests/rest_api/fixtures/init.py | 177 ++++++++++ tests/rest_api/test_analytics.py | 3 +- tests/rest_api/test_chache_policy.py | 2 +- .../rest_api/test_check_objects_integrity.py | 37 ++- tests/rest_api/test_cloud_storages.py | 9 +- tests/rest_api/test_invitations.py | 4 +- tests/rest_api/test_issues.py | 8 +- tests/rest_api/test_jobs.py | 16 +- tests/rest_api/test_memberships.py | 7 +- tests/rest_api/test_organizations.py | 10 +- tests/rest_api/test_projects.py | 7 +- tests/rest_api/test_remote_url.py | 4 +- tests/rest_api/test_tasks.py | 100 ++---- tests/rest_api/test_users.py | 6 +- tests/rest_api/utils/__init__.py | 3 + tests/rest_api/utils/config.py | 4 +- 31 files changed, 930 insertions(+), 595 deletions(-) create mode 100644 site/content/en/docs/contributing/running-tests.md rename tests/{cypress_cron_type.json => nightly_cypress.json} (100%) create mode 100644 tests/pr_cypress.json create mode 100644 tests/pr_cypress_canvas3d.json create mode 100644 tests/rest_api/fixtures/__init__.py create mode 100644 tests/rest_api/fixtures/data.py create mode 100644 tests/rest_api/fixtures/init.py create mode 100644 tests/rest_api/utils/__init__.py diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml index 7401b3d5..eed46e8a 100644 --- a/.github/workflows/cache.yml +++ b/.github/workflows/cache.yml @@ -24,7 +24,7 @@ jobs: ${{ runner.os }}-build-ui- - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1.1.2 + uses: docker/setup-buildx-action@v2 - name: Caching CVAT server uses: docker/build-push-action@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9624807d..3a11171d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,62 +7,51 @@ on: pull_request: types: [edited, ready_for_review, opened, synchronize, reopened] -jobs: - Unit_testing: - if: | - github.event.pull_request.draft == false && - !startsWith(github.event.pull_request.title, '[WIP]') && - !startsWith(github.event.pull_request.title, '[Dependent]') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-python@v2 - with: - python-version: '3.8' +env: + API_ABOUT_PAGE: "localhost:8080/api/server/about" +jobs: + cache: + if: | + github.event.pull_request.draft == false && + !startsWith(github.event.pull_request.title, '[WIP]') && + !startsWith(github.event.pull_request.title, '[Dependent]') + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.get-sha.outputs.sha}} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + steps: - name: Getting SHA from the default branch id: get-sha run: | - DEFAULT_BRANCH=$(curl -s \ - --request GET \ - --url https://api.github.com/repos/${{ github.repository }} \ - --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | \ - jq -r '.default_branch') - - SHA=$(curl -s \ - --request GET \ - --url https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH} \ - --header 'authorization: token ${{ secrets.GITHUB_TOKEN }}' | \ - jq -r '.object.sha') + DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch') + SHA=$(gh api /repos/$REPO/git/ref/heads/$DEFAULT_BRANCH | jq -r '.object.sha') echo ::set-output name=default_branch::${DEFAULT_BRANCH} echo ::set-output name=sha::${SHA} - name: Waiting a cache creation in the default branch - if: ${{ github.ref_name != 'develop' }} + env: + DEFAULT_BRANCH: ${{ steps.get-sha.outputs.default_branch }} + SHA: ${{ steps.get-sha.outputs.sha }} run: | SLEEP=45 NUMBER_ATTEMPTS=10 while [[ ${NUMBER_ATTEMPTS} -gt 0 ]]; do - RUN_status=$(curl -s \ - --request GET \ - --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ - --url https://api.github.com/repos/${{ github.repository }}/actions/workflows/cache.yml/runs | \ - jq -r '.workflow_runs[]? | - select( - (.head_sha == "${{ steps.get-sha.outputs.sha }}") - and (.event == "push") - and (.name == "Cache") - and (.head_branch == "${{ steps.get-sha.outputs.default_branch }}") - ) | .status') + + RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \ + jq -r ".workflow_runs[]? | + select((.head_sha == \"${SHA}\")) | .status") + if [[ ${RUN_status} == "completed" ]]; then - echo "The cache creation on the '${{ steps.get-sha.outputs.default_branch }}' branch has finished. Status: ${RUN_status}" + echo "The cache creation on the ${DEFAULT_BRANCH} branch has finished. Status: ${RUN_status}" break else echo "The creation of the cache is not yet complete." echo "There are still attempts to check the cache: ${NUMBER_ATTEMPTS}" - echo "Status of caching in the '${{ steps.get-sha.outputs.default_branch }}' branch: ${RUN_status}" + echo "Status of caching in the ${DEFAULT_BRANCH} branch: ${RUN_status}" echo "sleep ${SLEEP}" sleep ${SLEEP} ((NUMBER_ATTEMPTS--)) @@ -73,20 +62,30 @@ jobs: echo "Probably the creation of the cache is not yet complete. Will continue working without the cache." fi + Unit_testing: + needs: cache + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Getting CVAT server cache from the default branch uses: actions/cache@v2 with: path: /tmp/cvat_cache_server - key: ${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }} + key: ${{ runner.os }}-build-server-${{ needs.cache.outputs.sha }} - name: Getting CVAT UI cache from the default branch uses: actions/cache@v2 with: path: /tmp/cvat_cache_ui - key: ${{ runner.os }}-build-ui-${{ steps.get-sha.outputs.sha }} + key: ${{ runner.os }}-build-ui-${{ needs.cache.outputs.sha }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1.1.2 + uses: docker/setup-buildx-action@master - name: Building CVAT server image uses: docker/build-push-action@v2 @@ -113,19 +112,10 @@ jobs: ./opa test cvat/apps/iam/rules - name: Running REST API tests - env: - API_ABOUT_PAGE: "localhost:8080/api/server/about" - # Access key length should be at least 3, and secret key length at least 8 characters - MINIO_ACCESS_KEY: "minio_access_key" - MINIO_SECRET_KEY: "minio_secret_key" run: | - docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f components/analytics/docker-compose.analytics.yml -f tests/rest_api/docker-compose.minio.yml up -d - /bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done' - pip3 install --user -r tests/rest_api/requirements.txt - pytest tests/rest_api/ -k 'GET' - - docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f components/analytics/docker-compose.analytics.yml -f tests/rest_api/docker-compose.minio.yml down -v + pytest tests/rest_api/ -k 'GET' -s + pytest tests/rest_api/ --stop-services - name: Running unit tests env: @@ -137,6 +127,7 @@ jobs: docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ -c 'cd cvat-data && npm ci --ignore-scripts && cd ../cvat-core && npm ci --ignore-scripts && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info' + - name: Uploading code coverage results as an artifact if: github.ref == 'refs/heads/develop' uses: actions/upload-artifact@v2 @@ -147,10 +138,7 @@ jobs: ${{ github.workspace }}/lcov.info E2E_testing: - if: | - github.event.pull_request.draft == false && - !startsWith(github.event.pull_request.title, '[WIP]') && - !startsWith(github.event.pull_request.title, '[Dependent]') + needs: cache runs-on: ubuntu-latest strategy: fail-fast: false @@ -159,76 +147,24 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Getting SHA from the default branch - id: get-sha - run: | - DEFAULT_BRANCH=$(curl -s \ - --request GET \ - --url https://api.github.com/repos/${{ github.repository }} \ - --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | \ - jq -r '.default_branch') - - SHA=$(curl -s \ - --request GET \ - --url https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH} \ - --header 'authorization: token ${{ secrets.GITHUB_TOKEN }}' | \ - jq -r '.object.sha') - - echo ::set-output name=default_branch::${DEFAULT_BRANCH} - echo ::set-output name=sha::${SHA} - - - name: Waiting a cache creation in the default branch - run: | - URL_runs="https://api.github.com/repos/${{ github.repository }}/actions/workflows/cache.yml/runs" - SLEEP=45 - NUMBER_ATTEMPTS=10 - while [[ ${NUMBER_ATTEMPTS} -gt 0 ]]; do - RUN_status=$(curl -s \ - --request GET \ - --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ - --url https://api.github.com/repos/${{ github.repository }}/actions/workflows/cache.yml/runs | \ - jq -r '.workflow_runs[]? | - select( - (.head_sha == "${{ steps.get-sha.outputs.sha }}") - and (.event == "push") - and (.name == "Cache") - and (.head_branch == "${{ steps.get-sha.outputs.default_branch }}") - ) | .status') - if [[ ${RUN_status} == "completed" ]]; then - echo "The cache creation on the '${{ steps.get-sha.outputs.default_branch }}' branch has finished. Status: ${RUN_status}" - break - else - echo "The creation of the cache is not yet complete." - echo "There are still attempts to check the cache: ${NUMBER_ATTEMPTS}" - echo "Status of caching in the '${{ steps.get-sha.outputs.default_branch }}' branch: ${RUN_status}" - echo "sleep ${SLEEP}" - sleep ${SLEEP} - ((NUMBER_ATTEMPTS--)) - fi - done - if [[ ${NUMBER_ATTEMPTS} -eq 0 ]]; then - echo "Number of attempts expired!" - echo "Probably the creation of the cache is not yet complete. Will continue working without the cache." - fi - - name: Getting CVAT server cache from the default branch uses: actions/cache@v2 with: path: /tmp/cvat_cache_server - key: ${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }} + key: ${{ runner.os }}-build-server-${{ needs.cache.outputs.sha }} - name: Getting CVAT UI cache from the default branch uses: actions/cache@v2 with: path: /tmp/cvat_cache_ui - key: ${{ runner.os }}-build-ui-${{ steps.get-sha.outputs.sha }} + key: ${{ runner.os }}-build-ui-${{ needs.cache.outputs.sha }} - uses: actions/setup-node@v2 with: node-version: '16.x' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1.1.2 + uses: docker/setup-buildx-action@master - name: Building CVAT server image uses: docker/build-push-action@v2 @@ -260,35 +196,27 @@ jobs: DJANGO_SU_NAME: 'admin' DJANGO_SU_EMAIL: 'admin@localhost.company' DJANGO_SU_PASSWORD: '12qwaszx' - API_ABOUT_PAGE: "localhost:8080/api/server/about" run: | - docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f tests/docker-compose.file_share.yml up -d - /bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done' - docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell" + docker-compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + -f components/serverless/docker-compose.serverless.yml \ + -f tests/docker-compose.file_share.yml up -d + + /bin/bash -c \ + 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${{ env.API_ABOUT_PAGE }}) != "401" ]]; do sleep 5; done' + + docker exec -i cvat \ + /bin/bash -c \ + "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell" cd ./tests npm ci if [ ${{ matrix.specs }} == 'canvas3d_functionality' ]; then - # Choosing 5 test files - selected_files=$(find ./cypress/integration | grep -e 'case.*\|issue.*' | grep js | grep 3d | sort | head -5 | tr '\n' ',') - - npx cypress run \ - --headed \ - --browser chrome \ - --env coverage=false \ - --config-file cypress_canvas3d.json \ - --spec "${selected_files} cypress/integration/remove_users_tasks_projects_organizations.js" + npx cypress run --headed --browser chrome --config-file pr_cypress_canvas3d.json else - # Choosing 20 test files - find ./cypress/integration | grep -e 'case.*\|issue.*' | grep js | sed '/.*3d.*/d' | sort > test_files - selected_files=$({ head -10; tail -10;} < test_files | tr '\n' ',') - rm test_files - - npx cypress run \ - --browser chrome \ - --env coverage=false \ - --spec "${selected_files} cypress/integration/remove_users_tasks_projects_organizations.js" + npx cypress run --browser chrome --config-file pr_cypress.json fi - name: Creating a log file from "cvat" container logs @@ -323,30 +251,28 @@ jobs: needs: [Unit_testing, E2E_testing] steps: - uses: actions/checkout@v2 + - name: Getting SHA from the default branch id: get-sha + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} run: | - DEFAULT_BRANCH=$(curl -s \ - --request GET \ - --url https://api.github.com/repos/${{ github.repository }} \ - --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | \ - jq -r '.default_branch') - - SHA=$(curl -s \ - --request GET \ - --url https://api.github.com/repos/${{ github.repository }}/git/ref/heads/${DEFAULT_BRANCH} \ - --header 'authorization: token ${{ secrets.GITHUB_TOKEN }}' | \ - jq -r '.object.sha') + DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch') + SHA=$(gh api /repos/$REPO/git/ref/heads/$DEFAULT_BRANCH | jq -r '.object.sha') echo ::set-output name=default_branch::${DEFAULT_BRANCH} echo ::set-output name=sha::${SHA} + - name: Getting CVAT server cache from the default branch uses: actions/cache@v2 with: path: /tmp/cvat_cache_server key: ${{ runner.os }}-build-server-${{ steps.get-sha.outputs.sha }} + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1.1.2 + - name: Building CVAT server image uses: docker/build-push-action@v2 with: @@ -355,10 +281,12 @@ jobs: cache-from: type=local,src=/tmp/cvat_cache_server tags: openvino/cvat_server:latest load: true + - name: Downloading coverage results uses: actions/download-artifact@v2 with: name: coverage_results + - name: Combining coverage results run: | mkdir -p ./nyc_output_tmp @@ -366,6 +294,7 @@ jobs: mkdir -p ./.nyc_output npm ci npx nyc merge ./nyc_output_tmp ./.nyc_output/out.json + - name: Sending results to Coveralls env: HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index 7f6d7005..443fb76d 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -4,13 +4,132 @@ on: - cron: '0 22 * * *' workflow_dispatch: jobs: - build: + check_updates: + runs-on: ubuntu-latest + outputs: + last_commit_time: ${{ steps.check_updates.outputs.last_commit_time }} + last_night_time: ${{ steps.check_updates.outputs.last_night_time }} + steps: + - id: check_updates + env: + REPO: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + default_branch=$(gh api /repos/$REPO | jq -r '.default_branch') + + last_commit_time=$(date +%s \ + -d $(gh api /repos/${REPO}/branches/${default_branch} | jq -r '.commit.commit.author.date')) + + last_night_time=$(date +%s \ + -d $(gh api /repos/${REPO}/actions/workflows/caching.yml/runs | jq -r '.workflow_runs[].updated_at' | sort | tail -1)) + + echo ::set-output name=last_commit_time::${last_commit_time} + echo ::set-output name=last_night_time::${last_night_time} + + cache: + needs: check_updates + if: + needs.check_updates.outputs.last_commit_time > needs.check_updates.outputs.last_night_time + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.get-sha.outputs.sha}} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + steps: + - name: Getting SHA from the default branch + id: get-sha + run: | + DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch') + SHA=$(gh api /repos/$REPO/git/ref/heads/$DEFAULT_BRANCH | jq -r '.object.sha') + + echo ::set-output name=default_branch::${DEFAULT_BRANCH} + echo ::set-output name=sha::${SHA} + + - name: Waiting a cache creation in the default branch + if: ${{ github.ref_name != 'develop' }} + env: + DEFAULT_BRANCH: ${{ steps.get-sha.outputs.default_branch }} + SHA: ${{ steps.get-sha.outputs.sha }} + run: | + SLEEP=45 + NUMBER_ATTEMPTS=10 + while [[ ${NUMBER_ATTEMPTS} -gt 0 ]]; do + + RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \ + jq -r ".workflow_runs[]? | + select((.head_sha == \"${SHA}\")) | .status") + + if [[ ${RUN_status} == "completed" ]]; then + echo "The cache creation on the ${DEFAULT_BRANCH} branch has finished. Status: ${RUN_status}" + break + else + echo "The creation of the cache is not yet complete." + echo "There are still attempts to check the cache: ${NUMBER_ATTEMPTS}" + echo "Status of caching in the ${DEFAULT_BRANCH} branch: ${RUN_status}" + echo "sleep ${SLEEP}" + sleep ${SLEEP} + ((NUMBER_ATTEMPTS--)) + fi + done + if [[ ${NUMBER_ATTEMPTS} -eq 0 ]]; then + echo "Number of attempts expired!" + echo "Probably the creation of the cache is not yet complete. Will continue working without the cache." + fi + + run_tests: + needs: cache runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 with: node-version: '16.x' + + - uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Getting CVAT server cache from the default branch + uses: actions/cache@v2 + with: + path: /tmp/cvat_cache_server + key: ${{ runner.os }}-build-server-${{ needs.cache.outputs.sha }} + + - name: Getting CVAT UI cache from the default branch + uses: actions/cache@v2 + with: + path: /tmp/cvat_cache_ui + key: ${{ runner.os }}-build-ui-${{ needs.cache.outputs.sha }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Building CVAT server image + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + cache-from: type=local,src=/tmp/cvat_cache_server + tags: openvino/cvat_server:latest + load: true + + - name: Building CVAT UI image + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile.ui + cache-from: type=local,src=/tmp/cvat_cache_ui + tags: openvino/cvat_ui:latest + load: true + + - name: Running REST API tests + run: | + pip3 install --user -r tests/rest_api/requirements.txt + pytest tests/rest_api/ + pytest tests/rest_api/ --stop-services + - name: Build CVAT env: DJANGO_SU_NAME: "admin" @@ -21,11 +140,21 @@ jobs: docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f ./tests/docker-compose.email.yml -f tests/docker-compose.file_share.yml -f components/serverless/docker-compose.serverless.yml up -d --build /bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done' docker exec -i cvat /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell" + - name: End-to-end testing run: | cd ./tests npm ci npm run cypress:run:firefox + + - name: Unit tests + run: | + python manage.py test cvat/apps utils/cli + + npm ci + cd cvat-core + npm run test + - name: Uploading cypress screenshots as an artifact if: failure() uses: actions/upload-artifact@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe6c3c3..c8c40a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bumped nuclio version to 1.8.14 () +- Simplified running REST API tests. Extended CI-nightly workflow () + ### Deprecated - TDB diff --git a/site/content/en/docs/contributing/development-environment.md b/site/content/en/docs/contributing/development-environment.md index 7150d1f4..3bca39ce 100644 --- a/site/content/en/docs/contributing/development-environment.md +++ b/site/content/en/docs/contributing/development-environment.md @@ -134,6 +134,7 @@ description: 'Installing a development environment for different operating syste You have done! Now it is possible to insert breakpoints and debug server and client of the tool. +Instructions for running tests locally are available [here](/site/content/en/docs/contributing/running-tests.md). ## Note for Windows users diff --git a/site/content/en/docs/contributing/running-tests.md b/site/content/en/docs/contributing/running-tests.md new file mode 100644 index 00000000..7b19ee07 --- /dev/null +++ b/site/content/en/docs/contributing/running-tests.md @@ -0,0 +1,94 @@ +--- +title: 'Running tests' +linkTitle: 'Running tests' +weight: 11 +description: 'Instructions on how to run all existence tests.' +--- + +# E2E tests + +**Initial steps**: +1. Run CVAT instance: + ``` + docker-compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + -f components/serverless/docker-compose.serverless.yml \ + -f tests/docker-compose.file_share.yml up -d + ``` +1. Add test user in CVAT: + ``` + docker exec -i cvat \ + /bin/bash -c \ + "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@localhost.company', '12qwaszx')\" | python3 ~/manage.py shell" + ``` +1. Install npm dependencies: + ``` + cd tests + npm ci + ``` + +**Running tests** + +``` +npm run cypress:run:chrome +npm run cypress:run:chrome:canvas3d +``` + +# REST API tests + +**Initial steps** +1. Install all necessary requirements before running REST API tests: + ``` + pip install -r ./tests/rest_api/requirements.txt + ``` + +**Running tests** + +Run all REST API tests: + +``` +pytest ./tests/rest_api +``` + +This command will automatically start all necessary docker containers. + +If you want to start/stop these containers without running tests +use special options for it: + +``` +pytest ./tests/rest_api --start-services +pytest ./tests/rest_api --stop-services +``` + +If you need to rebuild your CVAT images add `--rebuild` option: +``` +pytest ./tests/rest_api --rebuild +``` + +# Unit tests + +**Initial steps** +1. Install necessary Python dependencies: + ``` + pip install -r cvat/requirements/testing.txt + ``` +1. Install npm dependencies: + ``` + npm ci + ``` +1. Run CVAT instance + ``` + docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d + ``` + +**Running tests** +1. Python tests + ``` + python manage.py test --settings cvat.settings.testing cvat/apps utils/cli + ``` +1. JS tests + ``` + cd cvat-core + npm run test + ``` diff --git a/tests/cypress_cron_type.json b/tests/nightly_cypress.json similarity index 100% rename from tests/cypress_cron_type.json rename to tests/nightly_cypress.json diff --git a/tests/pr_cypress.json b/tests/pr_cypress.json new file mode 100644 index 00000000..97773d04 --- /dev/null +++ b/tests/pr_cypress.json @@ -0,0 +1,37 @@ +{ + "video": false, + "baseUrl": "http://localhost:8080", + "viewportWidth": 1300, + "viewportHeight": 960, + "defaultCommandTimeout": 25000, + "downloadsFolder": "cypress/fixtures", + "env": { + "user": "admin", + "email": "admin@localhost.company", + "password": "12qwaszx", + "coverage": false + }, + "testFiles": [ + "actions_objects2/case_108_rotated_bounding_boxes.js", + "actions_objects2/case_10_polygon_shape_track_label_points.js", + "actions_objects2/case_115_ellipse_shape_track_label.js", + "actions_objects2/case_11_polylines_shape_track_label_points.js", + "actions_objects2/case_12_points_shape_track_label.js", + "actions_objects2/case_13_merge_split_features.js", + "actions_objects2/case_14_appearance_features.js", + "actions_objects2/case_15_group_features.js", + "actions_objects2/case_16_z_order_features.js", + "actions_objects2/case_17_lock_hide_features.js", + "issues_prs/issue_2418_object_tag_same_labels.js", + "issues_prs/issue_2485_navigation_empty_frames.js", + "issues_prs/issue_2486_not_edit_object_aam.js", + "issues_prs/issue_2487_extra_instances_canvas_grouping.js", + "issues_prs/issue_2661_displaying_attached_files_when_creating_task.js", + "issues_prs/issue_2753_call_HOC_component_each_render.js", + "issues_prs/issue_2807_polyline_editing.js", + "issues_prs/issue_2992_crop_polygon_properly.js", + "issues_prs/pr_1370_check_UI_fail_with_object_dragging_and_go_next_frame.js", + "issues_prs/pr_2203_error_cannot_read_property_at_saving_job.js", + "remove_users_tasks_projects_organizations.js" + ] +} diff --git a/tests/pr_cypress_canvas3d.json b/tests/pr_cypress_canvas3d.json new file mode 100644 index 00000000..99a3363c --- /dev/null +++ b/tests/pr_cypress_canvas3d.json @@ -0,0 +1,22 @@ +{ + "video": false, + "baseUrl": "http://localhost:8080", + "viewportWidth": 1300, + "viewportHeight": 960, + "defaultCommandTimeout": 25000, + "downloadsFolder": "cypress/fixtures", + "env": { + "user": "admin", + "email": "admin@localhost.company", + "password": "12qwaszx", + "coverage": false + }, + "testFiles": [ + "actions_projects_models/case_104_project_export_3d.js", + "canvas3d_functionality_2/case_56_canvas3d_functionality_basic_actions.js", + "canvas3d_functionality_2/case_62_canvas3d_functionality_views_resize.js", + "canvas3d_functionality_2/case_63_canvas3d_functionality_control_button_mouse_interaction.js", + "canvas3d_functionality_2/case_64_canvas3d_functionality_cuboid.js", + "remove_users_tasks_projects_organizations.js" + ] +} diff --git a/tests/rest_api/README.md b/tests/rest_api/README.md index 4d50ea26..91fb84f6 100644 --- a/tests/rest_api/README.md +++ b/tests/rest_api/README.md @@ -19,12 +19,6 @@ the server calling REST API directly (as it done by users). ## How to run? -1. Execute commands below to run docker containers: - ```console - export MINIO_ACCESS_KEY="minio_access_key" - export MINIO_SECRET_KEY="minio_secret_key" - docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/analytics/docker-compose.analytics.yml -f tests/rest_api/docker-compose.minio.yml up -d --build - ``` 1. After that please look at documentation for [pytest](https://docs.pytest.org/en/6.2.x/). Generally, you have to install requirements and run the following command from the root directory of the cloned CVAT repository: @@ -34,6 +28,9 @@ the server calling REST API directly (as it done by users). pytest tests/rest_api/ ``` + See the [contributing guide](../../site/content/en/docs/contributing/running-tests.md) + to get more information about tests running. + ## How to upgrade testing assets? When you have a new use case which cannot be expressed using objects already @@ -69,8 +66,8 @@ for i, color in enumerate(colormap): To backup DB and data volume, please use commands below. ```console -docker exec cvat python manage.py dumpdata --indent 2 > assets/cvat_db/data.json -docker exec cvat tar -cjv /home/django/data > assets/cvat_db/cvat_data.tar.bz2 +docker exec test_cvat_1 python manage.py dumpdata --indent 2 > assets/cvat_db/data.json +docker exec test_cvat_1 tar -cjv /home/django/data > assets/cvat_db/cvat_data.tar.bz2 ``` > Note: if you won't be use --indent options or will be use with other value @@ -90,8 +87,8 @@ python utils/dump_objects.py To restore DB and data volume, please use commands below. ```console -cat assets/cvat_db/data.json | docker exec -i cvat python manage.py loaddata --format=json - -cat assets/cvat_db/cvat_data.tar.bz2 | docker exec -i cvat tar --strip 3 -C /home/django/data/ -xj +cat assets/cvat_db/data.json | docker exec -i test_cvat_1 python manage.py loaddata --format=json - +cat assets/cvat_db/cvat_data.tar.bz2 | docker exec -i test_cvat_1 tar --strip 3 -C /home/django/data/ -xj ``` ## Assets directory structure @@ -173,9 +170,9 @@ Assets directory has two parts: 1. If your test infrastructure has been corrupted and you have errors during db restoring. You should to create (or recreate) `cvat` database: ``` - docker exec cvat_db dropdb --if-exists cvat - docker exec cvat_db createdb cvat - docker exec cvat python manage.py migrate + docker exec test_cvat_db_1 dropdb --if-exists cvat + docker exec test_cvat_db_1 createdb cvat + docker exec test_cvat_1 python manage.py migrate ``` 1. Perform migrate when some relation does not exists. Example of error message: @@ -184,7 +181,7 @@ Assets directory has two parts: ``` Solution: ``` - docker exec cvat python manage.py migrate + docker exec test_cvat_1 python manage.py migrate ``` 1. If for some reason you need to recreate cvat database, but using `dropdb` @@ -196,6 +193,6 @@ Assets directory has two parts: In this case you should terminate all existent connections for cvat database, you can perform it with command: ``` - docker exec cvat_db psql -U root -d postgres -v from=cvat -v to=test_db -f restore.sql + docker exec test_cvat_db_1 psql -U root -d postgres -v from=cvat -v to=test_db -f restore.sql ``` diff --git a/tests/rest_api/assets/cvat_db/cvat_data.tar.bz2 b/tests/rest_api/assets/cvat_db/cvat_data.tar.bz2 index cb9f2f9eb6a9181156641b9616d764fafed9206c..38ae1ac135025ad712271f9f0e938578c937e994 100644 GIT binary patch literal 75638 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 D=sX#c literal 45841 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�wZAm6WLjj(?`X;6~JjTc6#sSah+AO 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 diff --git a/tests/rest_api/conftest.py b/tests/rest_api/conftest.py index b84513f7..02b0e448 100644 --- a/tests/rest_api/conftest.py +++ b/tests/rest_api/conftest.py @@ -1,306 +1,2 @@ -# Copyright (C) 2021 Intel Corporation -# -# SPDX-License-Identifier: MIT -from subprocess import run, CalledProcessError -import pytest -import json -import os.path as osp -from .utils.config import ASSETS_DIR - -CVAT_DB_DIR = osp.join(ASSETS_DIR, 'cvat_db') - -def _run(command): - try: - run(command.split(), check=True) #nosec - except CalledProcessError: - pytest.exit(f'Command failed: {command}. Add `-s` option to see more details') - -def restore_data_volume(): - _run(f"docker container cp {osp.join(ASSETS_DIR, 'cvat_db', 'cvat_data.tar.bz2')} cvat:cvat_data.tar.bz2") - _run(f"docker exec -i cvat tar --strip 3 -xjf /cvat_data.tar.bz2 -C /home/django/data/") - -def create_test_db(): - _run(f"docker container cp {osp.join(CVAT_DB_DIR, 'restore.sql')} cvat_db:restore.sql") - _run(f"docker container cp {osp.join(CVAT_DB_DIR, 'data.json')} cvat:data.json") - _run('docker exec cvat python manage.py loaddata /data.json') - _run('docker exec cvat_db psql -U root -d postgres -v from=cvat -v to=test_db -f restore.sql') - -@pytest.fixture(scope='session', autouse=True) -def init_test_db(): - restore_data_volume() - create_test_db() - - yield - - _run('docker exec cvat_db psql -U root -d postgres -v from=test_db -v to=cvat -f restore.sql') - _run('docker exec cvat_db dropdb test_db') - -@pytest.fixture(scope='function') -def restore(): - _run('docker exec cvat_db psql -U root -d postgres -v from=test_db -v to=cvat -f restore.sql') - -@pytest.fixture(scope='function') -def restore_cvat_data(): - restore_data_volume() - -class Container: - def __init__(self, data, key='id'): - self.raw_data = data - self.map_data = { obj[key]: obj for obj in data } - - @property - def raw(self): - return self.raw_data - - @property - def map(self): - return self.map_data - - def __iter__(self): - return iter(self.raw_data) - - def __len__(self): - return len(self.raw_data) - - def __getitem__(self, key): - if isinstance(key, slice): - return self.raw_data[key] - return self.map_data[key] - -@pytest.fixture(scope='module') -def users(): - with open(osp.join(ASSETS_DIR, 'users.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def organizations(): - with open(osp.join(ASSETS_DIR, 'organizations.json')) as f: - return Container(json.load(f)) - -@pytest.fixture(scope='module') -def memberships(): - with open(osp.join(ASSETS_DIR, 'memberships.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def tasks(): - with open(osp.join(ASSETS_DIR, 'tasks.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def projects(): - with open(osp.join(ASSETS_DIR, 'projects.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def jobs(): - with open(osp.join(ASSETS_DIR, 'jobs.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def invitations(): - with open(osp.join(ASSETS_DIR, 'invitations.json')) as f: - return Container(json.load(f)['results'], key='key') - -@pytest.fixture(scope='module') -def annotations(): - with open(osp.join(ASSETS_DIR, 'annotations.json')) as f: - return json.load(f) - -@pytest.fixture(scope='module') -def cloud_storages(): - with open(osp.join(ASSETS_DIR, 'cloudstorages.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def issues(): - with open(osp.join(ASSETS_DIR, 'issues.json')) as f: - return Container(json.load(f)['results']) - -@pytest.fixture(scope='module') -def users_by_name(users): - return {user['username']: user for user in users} - -@pytest.fixture(scope='module') -def jobs_by_org(tasks, jobs): - data = {} - for job in jobs: - data.setdefault(tasks[job['task_id']]['organization'], []).append(job) - data[''] = data.pop(None, []) - return data - -@pytest.fixture(scope='module') -def tasks_by_org(tasks): - data = {} - for task in tasks: - data.setdefault(task['organization'], []).append(task) - data[''] = data.pop(None, []) - return data - -@pytest.fixture(scope='module') -def issues_by_org(tasks, jobs, issues): - data = {} - for issue in issues: - data.setdefault(tasks[jobs[issue['job']]['task_id']]['organization'], []).append(issue) - data[''] = data.pop(None, []) - return data - -@pytest.fixture(scope='module') -def assignee_id(): - def get_id(data): - if data.get('assignee') is not None: - return data['assignee']['id'] - return get_id - -def ownership(func): - def wrap(user_id, resource_id): - if resource_id is None: - return False - return func(user_id, resource_id) - return wrap - -@pytest.fixture(scope='module') -def is_project_staff(projects, assignee_id): - @ownership - def check(user_id, pid): - return user_id == projects[pid]['owner']['id'] or \ - user_id == assignee_id(projects[pid]) - return check - -@pytest.fixture(scope='module') -def is_task_staff(tasks, is_project_staff, assignee_id): - @ownership - def check(user_id, tid): - return user_id == tasks[tid]['owner']['id'] or \ - user_id == assignee_id(tasks[tid]) or \ - is_project_staff(user_id, tasks[tid]['project_id']) - return check - -@pytest.fixture(scope='module') -def is_job_staff(jobs, is_task_staff, assignee_id): - @ownership - def check(user_id, jid): - return user_id == assignee_id(jobs[jid]) or \ - is_task_staff(user_id, jobs[jid]['task_id']) - return check - -@pytest.fixture(scope='module') -def is_issue_staff(issues, jobs, assignee_id): - @ownership - def check(user_id, issue_id): - return user_id == issues[issue_id]['owner']['id'] or \ - user_id == assignee_id(issues[issue_id]) or \ - user_id == assignee_id(jobs[issues[issue_id]['job']]) - return check - -@pytest.fixture(scope='module') -def is_issue_admin(issues, jobs, is_task_staff): - @ownership - def check(user_id, issue_id): - return is_task_staff(user_id, jobs[issues[issue_id]['job']]['task_id']) - return check - -@pytest.fixture(scope='module') -def find_users(test_db): - def find(**kwargs): - assert len(kwargs) > 0 - assert any(kwargs.values()) - - data = test_db - kwargs = dict(filter(lambda a: a[1] is not None, kwargs.items())) - for field, value in kwargs.items(): - if field.startswith('exclude_'): - field = field.split('_', maxsplit=1)[1] - exclude_rows = set(v['id'] for v in - filter(lambda a: a[field] == value, test_db)) - data = list(filter(lambda a: a['id'] not in exclude_rows, data)) - else: - data = list(filter(lambda a: a[field] == value, data)) - - return data - return find - -@pytest.fixture(scope='module') -def test_db(users, users_by_name, memberships): - data = [] - fields = ['username', 'id', 'privilege', 'role', 'org', 'membership_id'] - def add_row(**kwargs): - data.append({field: kwargs.get(field) for field in fields}) - - for user in users: - for group in user['groups']: - add_row(username=user['username'], id=user['id'], privilege=group) - - for membership in memberships: - username = membership['user']['username'] - for group in users_by_name[username]['groups']: - add_row(username=username, role=membership['role'], privilege=group, - id=membership['user']['id'], org=membership['organization'], - membership_id=membership['id']) - - return data - -@pytest.fixture(scope='module') -def org_staff(memberships): - def find(org_id): - if org_id in ['', None]: - return set() - else: - return set(m['user']['id'] for m in memberships - if m['role'] in ['maintainer', 'owner'] and m['user'] is not None - and m['organization'] == org_id) - return find - -@pytest.fixture(scope='module') -def is_org_member(memberships): - def check(user_id, org_id): - if org_id in ['', None]: - return True - else: - return user_id in set(m['user']['id'] for m in memberships - if m['user'] is not None and m['organization'] == org_id) - return check - -@pytest.fixture(scope='module') -def find_job_staff_user(is_job_staff): - def find(jobs, users, is_staff): - for job in jobs: - for user in users: - if is_staff == is_job_staff(user['id'], job['id']): - return user['username'], job['id'] - return None, None - return find - -@pytest.fixture(scope='module') -def find_task_staff_user(is_task_staff): - def find(tasks, users, is_staff): - for task in tasks: - for user in users: - if is_staff == is_task_staff(user['id'], task['id']): - return user['username'], task['id'] - return None, None - return find - -@pytest.fixture(scope='module') -def find_issue_staff_user(is_issue_staff, is_issue_admin): - def find(issues, users, is_staff, is_admin): - for issue in issues: - for user in users: - i_admin, i_staff = is_issue_admin(user['id'], issue['id']), is_issue_staff(user['id'], issue['id']) - if (is_admin is None and (i_staff or i_admin) == is_staff) \ - or (is_admin == i_admin and is_staff == i_staff): - return user['username'], issue['id'] - return None, None - return find - -@pytest.fixture(scope='module') -def filter_jobs_with_shapes(annotations): - def find(jobs): - return list(filter(lambda j: annotations['job'][str(j['id'])]['shapes'], jobs)) - return find - -@pytest.fixture(scope='module') -def filter_tasks_with_shapes(annotations): - def find(tasks): - return list(filter(lambda t: annotations['task'][str(t['id'])]['shapes'], tasks)) - return find +from .fixtures.init import * +from .fixtures.data import * diff --git a/tests/rest_api/docker-compose.minio.yml b/tests/rest_api/docker-compose.minio.yml index ebc7c746..ccc7d7d2 100644 --- a/tests/rest_api/docker-compose.minio.yml +++ b/tests/rest_api/docker-compose.minio.yml @@ -13,8 +13,8 @@ services: - 9000:9000 - 9001:9001 environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + MINIO_ROOT_USER: "minio_access_key" + MINIO_ROOT_PASSWORD: "minio_secret_key" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s @@ -31,8 +31,8 @@ services: environment: MC_PATH: "/usr/bin/mc" MINIO_HOST: "http://minio:9000" - MINIO_ACCESS_KEY: - MINIO_SECRET_KEY: + MINIO_ACCESS_KEY: "minio_access_key" + MINIO_SECRET_KEY: "minio_secret_key" MINIO_ALIAS: "local_minio" PRIVATE_BUCKET: "private" PUBLIC_BUCKET: "public" diff --git a/tests/rest_api/fixtures/__init__.py b/tests/rest_api/fixtures/__init__.py new file mode 100644 index 00000000..0aa5e58c --- /dev/null +++ b/tests/rest_api/fixtures/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT diff --git a/tests/rest_api/fixtures/data.py b/tests/rest_api/fixtures/data.py new file mode 100644 index 00000000..e4a1f738 --- /dev/null +++ b/tests/rest_api/fixtures/data.py @@ -0,0 +1,276 @@ +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import pytest +import json +import os.path as osp +from rest_api.utils.config import ASSETS_DIR + +CVAT_DB_DIR = osp.join(ASSETS_DIR, 'cvat_db') + +class Container: + def __init__(self, data, key='id'): + self.raw_data = data + self.map_data = { obj[key]: obj for obj in data } + + @property + def raw(self): + return self.raw_data + + @property + def map(self): + return self.map_data + + def __iter__(self): + return iter(self.raw_data) + + def __len__(self): + return len(self.raw_data) + + def __getitem__(self, key): + if isinstance(key, slice): + return self.raw_data[key] + return self.map_data[key] + +@pytest.fixture(scope='session') +def users(): + with open(osp.join(ASSETS_DIR, 'users.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def organizations(): + with open(osp.join(ASSETS_DIR, 'organizations.json')) as f: + return Container(json.load(f)) + +@pytest.fixture(scope='session') +def memberships(): + with open(osp.join(ASSETS_DIR, 'memberships.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def tasks(): + with open(osp.join(ASSETS_DIR, 'tasks.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def projects(): + with open(osp.join(ASSETS_DIR, 'projects.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def jobs(): + with open(osp.join(ASSETS_DIR, 'jobs.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def invitations(): + with open(osp.join(ASSETS_DIR, 'invitations.json')) as f: + return Container(json.load(f)['results'], key='key') + +@pytest.fixture(scope='session') +def annotations(): + with open(osp.join(ASSETS_DIR, 'annotations.json')) as f: + return json.load(f) + +@pytest.fixture(scope='session') +def cloud_storages(): + with open(osp.join(ASSETS_DIR, 'cloudstorages.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def issues(): + with open(osp.join(ASSETS_DIR, 'issues.json')) as f: + return Container(json.load(f)['results']) + +@pytest.fixture(scope='session') +def users_by_name(users): + return {user['username']: user for user in users} + +@pytest.fixture(scope='session') +def jobs_by_org(tasks, jobs): + data = {} + for job in jobs: + data.setdefault(tasks[job['task_id']]['organization'], []).append(job) + data[''] = data.pop(None, []) + return data + +@pytest.fixture(scope='session') +def tasks_by_org(tasks): + data = {} + for task in tasks: + data.setdefault(task['organization'], []).append(task) + data[''] = data.pop(None, []) + return data + +@pytest.fixture(scope='session') +def issues_by_org(tasks, jobs, issues): + data = {} + for issue in issues: + data.setdefault(tasks[jobs[issue['job']]['task_id']]['organization'], []).append(issue) + data[''] = data.pop(None, []) + return data + +@pytest.fixture(scope='session') +def assignee_id(): + def get_id(data): + if data.get('assignee') is not None: + return data['assignee']['id'] + return get_id + +def ownership(func): + def wrap(user_id, resource_id): + if resource_id is None: + return False + return func(user_id, resource_id) + return wrap + +@pytest.fixture(scope='session') +def is_project_staff(projects, assignee_id): + @ownership + def check(user_id, pid): + return user_id == projects[pid]['owner']['id'] or \ + user_id == assignee_id(projects[pid]) + return check + +@pytest.fixture(scope='session') +def is_task_staff(tasks, is_project_staff, assignee_id): + @ownership + def check(user_id, tid): + return user_id == tasks[tid]['owner']['id'] or \ + user_id == assignee_id(tasks[tid]) or \ + is_project_staff(user_id, tasks[tid]['project_id']) + return check + +@pytest.fixture(scope='session') +def is_job_staff(jobs, is_task_staff, assignee_id): + @ownership + def check(user_id, jid): + return user_id == assignee_id(jobs[jid]) or \ + is_task_staff(user_id, jobs[jid]['task_id']) + return check + +@pytest.fixture(scope='session') +def is_issue_staff(issues, jobs, assignee_id): + @ownership + def check(user_id, issue_id): + return user_id == issues[issue_id]['owner']['id'] or \ + user_id == assignee_id(issues[issue_id]) or \ + user_id == assignee_id(jobs[issues[issue_id]['job']]) + return check + +@pytest.fixture(scope='session') +def is_issue_admin(issues, jobs, is_task_staff): + @ownership + def check(user_id, issue_id): + return is_task_staff(user_id, jobs[issues[issue_id]['job']]['task_id']) + return check + +@pytest.fixture(scope='session') +def find_users(test_db): + def find(**kwargs): + assert len(kwargs) > 0 + assert any(kwargs.values()) + + data = test_db + kwargs = dict(filter(lambda a: a[1] is not None, kwargs.items())) + for field, value in kwargs.items(): + if field.startswith('exclude_'): + field = field.split('_', maxsplit=1)[1] + exclude_rows = set(v['id'] for v in + filter(lambda a: a[field] == value, test_db)) + data = list(filter(lambda a: a['id'] not in exclude_rows, data)) + else: + data = list(filter(lambda a: a[field] == value, data)) + + return data + return find + +@pytest.fixture(scope='session') +def test_db(users, users_by_name, memberships): + data = [] + fields = ['username', 'id', 'privilege', 'role', 'org', 'membership_id'] + def add_row(**kwargs): + data.append({field: kwargs.get(field) for field in fields}) + + for user in users: + for group in user['groups']: + add_row(username=user['username'], id=user['id'], privilege=group) + + for membership in memberships: + username = membership['user']['username'] + for group in users_by_name[username]['groups']: + add_row(username=username, role=membership['role'], privilege=group, + id=membership['user']['id'], org=membership['organization'], + membership_id=membership['id']) + + return data + +@pytest.fixture(scope='session') +def org_staff(memberships): + def find(org_id): + if org_id in ['', None]: + return set() + else: + return set(m['user']['id'] for m in memberships + if m['role'] in ['maintainer', 'owner'] and m['user'] != None + and m['organization'] == org_id) + return find + +@pytest.fixture(scope='session') +def is_org_member(memberships): + def check(user_id, org_id): + if org_id in ['', None]: + return True + else: + return user_id in set(m['user']['id'] for m in memberships + if m['user'] != None and m['organization'] == org_id) + return check + +@pytest.fixture(scope='session') +def find_job_staff_user(is_job_staff): + def find(jobs, users, is_staff): + for job in jobs: + for user in users: + if is_staff == is_job_staff(user['id'], job['id']): + return user['username'], job['id'] + return None, None + return find + +@pytest.fixture(scope='session') +def find_task_staff_user(is_task_staff): + def find(tasks, users, is_staff): + for task in tasks: + for user in users: + if is_staff == is_task_staff(user['id'], task['id']): + return user['username'], task['id'] + return None, None + return find + +@pytest.fixture(scope='session') +def find_issue_staff_user(is_issue_staff, is_issue_admin): + def find(issues, users, is_staff, is_admin): + for issue in issues: + for user in users: + i_admin, i_staff = is_issue_admin(user['id'], issue['id']), is_issue_staff(user['id'], issue['id']) + if (is_admin is None and (i_staff or i_admin) == is_staff) \ + or (is_admin == i_admin and is_staff == i_staff): + return user['username'], issue['id'] + return None, None + return find + +@pytest.fixture(scope='session') +def filter_jobs_with_shapes(annotations): + def find(jobs): + return list(filter(lambda j: annotations['job'][str(j['id'])]['shapes'], jobs)) + return find + +@pytest.fixture(scope='session') +def filter_tasks_with_shapes(annotations): + def find(tasks): + return list(filter(lambda t: annotations['task'][str(t['id'])]['shapes'], tasks)) + return find + +@pytest.fixture(scope='session') +def tasks_with_shapes(tasks, filter_tasks_with_shapes): + return filter_tasks_with_shapes(tasks) \ No newline at end of file diff --git a/tests/rest_api/fixtures/init.py b/tests/rest_api/fixtures/init.py new file mode 100644 index 00000000..31f519f0 --- /dev/null +++ b/tests/rest_api/fixtures/init.py @@ -0,0 +1,177 @@ +import os.path as osp +import re +from http import HTTPStatus +from subprocess import PIPE, CalledProcessError, run + +import pytest +import os +import requests +from rest_api.utils.config import ASSETS_DIR, get_api_url + +CVAT_ROOT_DIR = __file__[: __file__.rfind(osp.join("tests", ""))] +CVAT_DB_DIR = osp.join(ASSETS_DIR, "cvat_db") +PREFIX = "test" + +CONTAINER_NAME_FILES = [ + osp.join(CVAT_ROOT_DIR, dc_file) + for dc_file in ( + "components/analytics/docker-compose.analytics.tests.yml", + "docker-compose.tests.yml", + ) +] + +DC_FILES = [ + osp.join(CVAT_ROOT_DIR, dc_file) + for dc_file in ("docker-compose.dev.yml", "tests/rest_api/docker-compose.minio.yml") +] + CONTAINER_NAME_FILES + + +def pytest_addoption(parser): + group = parser.getgroup("CVAT REST API testing options") + group._addoption( + "--start-services", + action="store_true", + help="Start all necessary CVAT containers without running tests. (default: %(default)s)", + ) + + group._addoption( + "--stop-services", + action="store_true", + help="Stop all testing containers without running tests. (default: %(default)s)", + ) + + group._addoption( + "--rebuild", + action="store_true", + help="Rebuild CVAT images and then start containers. (default: %(default)s)", + ) + + group._addoption( + "--cleanup", + action="store_true", + help="Delete files that was create by tests without running tests. (default: %(default)s)", + ) + + +def _run(command): + try: + proc = run(command.split(), check=True, stdout=PIPE, stderr=PIPE) # nosec + return proc.stdout.decode(), proc.stderr.decode() + except CalledProcessError as exc: + pytest.exit( + f"Command failed: {command}.\n" + f"Error message: {exc.stderr.decode()}.\n" + f"Add `-s` option to see more details" + ) + + +def docker_cp(source, target): + _run(f"docker container cp {source} {target}") + + +def exec_cvat(command): + _run(f"docker exec {PREFIX}_cvat_1 {command}") + + +def exec_cvat_db(command): + _run(f"docker exec {PREFIX}_cvat_db_1 {command}") + + +def restore_db(): + exec_cvat_db("psql -U root -d postgres -v from=test_db -v to=cvat -f /tmp/restore.sql") + + +def create_compose_files(): + for filename in CONTAINER_NAME_FILES: + with open(filename.replace(".tests.yml", ".yml"), "r") as dcf, open(filename, "w") as ndcf: + ndcf.writelines( + [line for line in dcf.readlines() if not re.match("^.+container_name.+$", line)] + ) + + +def delete_compose_files(): + for filename in CONTAINER_NAME_FILES: + if osp.exists(filename): + os.remove(filename) + + +def wait_for_server(): + while True: + response = requests.get(get_api_url("users/self")) + if response.status_code == HTTPStatus.UNAUTHORIZED: + break + +def restore_data_volumes(): + docker_cp(osp.join(CVAT_DB_DIR, "cvat_data.tar.bz2"), f"{PREFIX}_cvat_1:/tmp/cvat_data.tar.bz2") + exec_cvat("tar --strip 3 -xjf /tmp/cvat_data.tar.bz2 -C /home/django/data/") + +def start_services(rebuild=False): + running_containers = [cn for cn in _run("docker ps --format {{.Names}}")[0].split("\n") if cn] + + if any([cn in ["cvat", "cvat_db"] for cn in running_containers]): + pytest.exit( + "It's looks like you already have running cvat containers. Stop them and try again. " + f"List of running containers: {', '.join(running_containers)}" + ) + + out = _run(f"docker-compose -p {PREFIX} -f {' -f '.join(DC_FILES)} up -d " + "--build" * rebuild)[1] + + restore_data_volumes() + docker_cp(osp.join(CVAT_DB_DIR, "restore.sql"), f"{PREFIX}_cvat_db_1:/tmp/restore.sql") + docker_cp(osp.join(CVAT_DB_DIR, "data.json"), f"{PREFIX}_cvat_1:/tmp/data.json") + + return out + + +@pytest.fixture(autouse=True, scope="session") +def services(request): + stop = request.config.getoption("--stop-services") + start = request.config.getoption("--start-services") + rebuild = request.config.getoption("--rebuild") + cleanup = request.config.getoption("--cleanup") + + if start and stop: + raise Exception("--start-services and --stop-services are incompatible") + + if cleanup: + delete_compose_files() + pytest.exit(f"All generated test files have been deleted", returncode=0) + + if not all([osp.exists(f) for f in CONTAINER_NAME_FILES]): + create_compose_files() + + if stop: + out = _run(f"docker-compose -p {PREFIX} -f {' -f '.join(DC_FILES)} down -v")[1] + out = set(l.split()[1] for l in out.split("\n") if "done" in l.split()) + pytest.exit(f"All testing containers are stopped: {', '.join(out)}", returncode=0) + + started_services = start_services(rebuild) + wait_for_server() + + exec_cvat("python manage.py loaddata /tmp/data.json") + exec_cvat_db("psql -U root -d postgres -v from=cvat -v to=test_db -f /tmp/restore.sql") + + if start: + pytest.exit( + f"All necessary containers have been created and started: {started_services}", + returncode=0, + ) + + yield + + restore_db() + exec_cvat_db("dropdb test_db") + + +@pytest.fixture(scope="function") +def changedb(): + restore_db() + + +@pytest.fixture(scope="class") +def dontchangedb(): + restore_db() + +@pytest.fixture(scope="function") +def restore_cvat_data(): + restore_data_volumes() diff --git a/tests/rest_api/test_analytics.py b/tests/rest_api/test_analytics.py index a5e76cc3..60685fe7 100644 --- a/tests/rest_api/test_analytics.py +++ b/tests/rest_api/test_analytics.py @@ -4,8 +4,9 @@ import pytest from http import HTTPStatus -from .utils.config import server_get +from rest_api.utils.config import server_get +@pytest.mark.usefixtures('dontchangedb') class TestGetAnalytics: endpoint = 'analytics/app/kibana' def _test_can_see(self, user): diff --git a/tests/rest_api/test_chache_policy.py b/tests/rest_api/test_chache_policy.py index 47fcf333..238ce8f6 100644 --- a/tests/rest_api/test_chache_policy.py +++ b/tests/rest_api/test_chache_policy.py @@ -4,7 +4,7 @@ from http import HTTPStatus import re -from .utils.config import server_get +from rest_api.utils.config import server_get class TestCachePolicy: diff --git a/tests/rest_api/test_check_objects_integrity.py b/tests/rest_api/test_check_objects_integrity.py index a8781060..c11ff086 100644 --- a/tests/rest_api/test_check_objects_integrity.py +++ b/tests/rest_api/test_check_objects_integrity.py @@ -6,23 +6,26 @@ import os.path as osp import glob import json from deepdiff import DeepDiff -from .utils import config +from rest_api.utils import config import pytest -@pytest.mark.parametrize('path', glob.glob(osp.join(config.ASSETS_DIR, '*.json'))) -def test_check_objects_integrity(path): - with open(path) as f: - endpoint = osp.basename(path).rsplit('.')[0] - if endpoint == 'annotations': - objects = json.load(f) - for jid, annotations in objects['job'].items(): - response = config.get_method('admin1', f'jobs/{jid}/annotations').json() - assert DeepDiff(annotations, response, ignore_order=True, - exclude_paths="root['version']") == {} - else: - response = config.get_method('admin1', endpoint, page_size='all') - json_objs = json.load(f) - resp_objs = response.json() +@pytest.mark.usefixtures('dontchangedb') +class TestGetResources: - assert DeepDiff(json_objs, resp_objs, ignore_order=True, - exclude_regex_paths=r"root\['results'\]\[\d+\]\['last_login'\]") == {} + @pytest.mark.parametrize('path', glob.glob(osp.join(config.ASSETS_DIR, '*.json'))) + def test_check_objects_integrity(self, path): + with open(path) as f: + endpoint = osp.basename(path).rsplit('.')[0] + if endpoint == 'annotations': + objects = json.load(f) + for jid, annotations in objects['job'].items(): + response = config.get_method('admin1', f'jobs/{jid}/annotations').json() + assert DeepDiff(annotations, response, ignore_order=True, + exclude_paths="root['version']") == {} + else: + response = config.get_method('admin1', endpoint, page_size='all') + json_objs = json.load(f) + resp_objs = response.json() + + assert DeepDiff(json_objs, resp_objs, ignore_order=True, + exclude_regex_paths=r"root\['results'\]\[\d+\]\['last_login'\]") == {} diff --git a/tests/rest_api/test_cloud_storages.py b/tests/rest_api/test_cloud_storages.py index 20dbbb07..1069c8e6 100644 --- a/tests/rest_api/test_cloud_storages.py +++ b/tests/rest_api/test_cloud_storages.py @@ -6,8 +6,9 @@ import pytest from http import HTTPStatus from deepdiff import DeepDiff -from .utils.config import get_method, patch_method, post_method +from rest_api.utils.config import get_method, patch_method, post_method +@pytest.mark.usefixtures('dontchangedb') class TestGetCloudStorage: def _test_can_see(self, user, storage_id, data, **kwargs): @@ -59,8 +60,8 @@ class TestGetCloudStorage: self._test_cannot_see(username, storage_id, org_id=org_id) -@pytest.mark.usefixtures("restore") -class TestPostCloudStorage: +@pytest.mark.usefixtures('changedb') +class TestPostCloudStorage(): _SPEC = { 'provider_type': 'AWS_S3_BUCKET', 'resource': 'test', @@ -121,7 +122,7 @@ class TestPostCloudStorage: else: self._test_cannot_create(username, self._SPEC, org_id=org_id) -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPatchCloudStorage: _SPEC = { 'display_name': 'New display name', diff --git a/tests/rest_api/test_invitations.py b/tests/rest_api/test_invitations.py index 32c7e4b8..fd327df5 100644 --- a/tests/rest_api/test_invitations.py +++ b/tests/rest_api/test_invitations.py @@ -4,9 +4,9 @@ from http import HTTPStatus import pytest -from .utils.config import post_method +from rest_api.utils.config import post_method -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestCreateInvitations: def _test_post_invitation_201(self, user, data, invitee, **kwargs): response = post_method(user, 'invitations', data, **kwargs) diff --git a/tests/rest_api/test_issues.py b/tests/rest_api/test_issues.py index b898037b..f2b02fee 100644 --- a/tests/rest_api/test_issues.py +++ b/tests/rest_api/test_issues.py @@ -7,9 +7,9 @@ from http import HTTPStatus from deepdiff import DeepDiff from copy import deepcopy -from .utils.config import post_method, patch_method +from rest_api.utils.config import post_method, patch_method -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPostIssues: def _test_check_response(self, user, data, is_allow, **kwargs): response = post_method(user, 'issues', data, **kwargs) @@ -78,9 +78,7 @@ class TestPostIssues: } self._test_check_response(username, data, is_allow, org_id=org) - - -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPatchIssues: def _test_check_response(self, user, issue_id, data, is_allow, **kwargs): response = patch_method(user, f'issues/{issue_id}', data, diff --git a/tests/rest_api/test_jobs.py b/tests/rest_api/test_jobs.py index be45a685..c336f3e1 100644 --- a/tests/rest_api/test_jobs.py +++ b/tests/rest_api/test_jobs.py @@ -5,7 +5,8 @@ from http import HTTPStatus from deepdiff import DeepDiff import pytest -from .utils.config import get_method, patch_method +from copy import deepcopy +from rest_api.utils.config import get_method, patch_method def get_job_staff(job, tasks, projects): job_staff = [] @@ -37,6 +38,7 @@ def filter_jobs(jobs, tasks, org): return jobs, kwargs +@pytest.mark.usefixtures('dontchangedb') class TestGetJobs: def _test_get_job_200(self, user, jid, data, **kwargs): response = get_method(user, f'jobs/{jid}', **kwargs) @@ -75,6 +77,7 @@ class TestGetJobs: else: self._test_get_job_403(user['username'], job['id'], **kwargs) +@pytest.mark.usefixtures('dontchangedb') class TestListJobs: def _test_list_jobs_200(self, user, data, **kwargs): response = get_method(user, 'jobs', **kwargs, page_size='all') @@ -110,6 +113,7 @@ class TestListJobs: else: self._test_list_jobs_403(user['username'], **kwargs) +@pytest.mark.usefixtures('dontchangedb') class TestGetAnnotations: def _test_get_job_annotations_200(self, user, jid, data, **kwargs): response = get_method(user, f'jobs/{jid}/annotations', **kwargs) @@ -180,7 +184,8 @@ class TestGetAnnotations: job_id, annotations['job'][str(job_id)], **kwargs) else: self._test_get_job_annotations_403(username, job_id, **kwargs) -@pytest.mark.usefixtures("restore") + +@pytest.mark.usefixtures('changedb') class TestPatchJobAnnotations: _ORG = 2 @@ -195,7 +200,7 @@ class TestPatchJobAnnotations: @pytest.fixture(scope='class') def request_data(self, annotations): def get_data(jid): - data = annotations['job'][str(jid)].copy() + data = deepcopy(annotations['job'][str(jid)]) data['shapes'][0].update({'points': [2.0, 3.0, 4.0, 5.0, 6.0, 7.0]}) data['version'] += 1 return data @@ -259,7 +264,7 @@ class TestPatchJobAnnotations: self._test_check_respone(is_allow, response, data) -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPatchJob: _ORG = 2 @@ -277,7 +282,7 @@ class TestPatchJob: def expected_data(self, jobs, users): keys = ['url', 'id', 'username', 'first_name', 'last_name'] def find(job_id, assignee_id): - data = jobs[job_id].copy() + data = deepcopy(jobs[job_id]) data['assignee'] = dict(filter(lambda a: a[0] in keys, users[assignee_id].items())) return data @@ -290,7 +295,6 @@ class TestPatchJob: members -= {assignee_id(jobs[jid]), user_id} return members.pop() return find_new_assignee - @pytest.mark.parametrize('org', [2]) @pytest.mark.parametrize('role, task_staff, is_allow', [ ('maintainer', False, True), ('owner', False, True), diff --git a/tests/rest_api/test_memberships.py b/tests/rest_api/test_memberships.py index 245f1607..10ccc6ca 100644 --- a/tests/rest_api/test_memberships.py +++ b/tests/rest_api/test_memberships.py @@ -6,8 +6,9 @@ import pytest from http import HTTPStatus from deepdiff import DeepDiff -from .utils.config import get_method, patch_method +from rest_api.utils.config import get_method, patch_method +@pytest.mark.usefixtures('dontchangedb') class TestGetMemberships: def _test_can_see_memberships(self, user, data, **kwargs): response = get_method(user, 'memberships', **kwargs) @@ -40,9 +41,7 @@ class TestGetMemberships: non_org1_users = ['user2', 'worker3'] for user in non_org1_users: self._test_cannot_see_memberships(user, org_id=1) - - -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPatchMemberships: _ORG = 2 diff --git a/tests/rest_api/test_organizations.py b/tests/rest_api/test_organizations.py index 4411883b..68bdc67c 100644 --- a/tests/rest_api/test_organizations.py +++ b/tests/rest_api/test_organizations.py @@ -4,8 +4,9 @@ from http import HTTPStatus import pytest -from .utils.config import get_method, options_method, patch_method, delete_method +from rest_api.utils.config import get_method, options_method, patch_method, delete_method from deepdiff import DeepDiff +from copy import deepcopy class TestMetadataOrganizations: _ORG = 2 @@ -33,6 +34,7 @@ class TestMetadataOrganizations: response = options_method(user, f'organizations/{self._ORG}') assert response.status_code == HTTPStatus.OK +@pytest.mark.usefixtures('dontchangedb') class TestGetOrganizations: _ORG = 2 @@ -60,7 +62,7 @@ class TestGetOrganizations: else: assert response.status_code == HTTPStatus.NOT_FOUND -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPatchOrganizations: _ORG = 2 @@ -71,7 +73,7 @@ class TestPatchOrganizations: @pytest.fixture(scope='class') def expected_data(self, organizations, request_data): - data = organizations[self._ORG].copy() + data = deepcopy(organizations[self._ORG]) data.update(request_data) return data @@ -101,7 +103,7 @@ class TestPatchOrganizations: else: assert response.status_code != HTTPStatus.OK -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestDeleteOrganizations: _ORG = 2 diff --git a/tests/rest_api/test_projects.py b/tests/rest_api/test_projects.py index 4b32f554..48356f4e 100644 --- a/tests/rest_api/test_projects.py +++ b/tests/rest_api/test_projects.py @@ -11,6 +11,7 @@ import pytest from .utils.config import get_method, post_files_method, post_method +@pytest.mark.usefixtures('dontchangedb') class TestGetProjects: def _find_project_by_user_org(self, user, projects, is_project_staff_flag, is_project_staff): if is_project_staff_flag: @@ -112,7 +113,7 @@ class TestGetProjects: self._test_response_200(user_in_project['username'], project_id, org_id=user_in_project['org']) -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPostProjects: def _test_create_project_201(self, user, spec, **kwargs): response = post_method(user, '/projects', spec, **kwargs) @@ -199,7 +200,7 @@ class TestPostProjects: } self._test_create_project_201(user['username'], spec, org_id=user['org']) -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures("changedb") @pytest.mark.usefixtures("restore_cvat_data") class TestImportExportDatasetProject: def _test_export_project(self, username, project_id, format_name): @@ -217,7 +218,7 @@ class TestImportExportDatasetProject: return response def _test_import_project(self, username, project_id, format_name, data): - response = post_files_method(username, f'projects/{project_id}/dataset', data, + response = post_files_method(username, f'projects/{project_id}/dataset', None, data, format=format_name) assert response.status_code == HTTPStatus.ACCEPTED diff --git a/tests/rest_api/test_remote_url.py b/tests/rest_api/test_remote_url.py index 241b9210..a11e8aff 100644 --- a/tests/rest_api/test_remote_url.py +++ b/tests/rest_api/test_remote_url.py @@ -7,7 +7,7 @@ from time import sleep import pytest -from .utils.config import get_method, post_method +from rest_api.utils.config import get_method, post_method def _post_task_remote_data(username, task_id, resources): @@ -29,7 +29,7 @@ def _wait_until_task_is_created(username, task_id): sleep(1) -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestGetAnalytics: task_id = 12 def _test_can_create(self, user, task_id, resources): diff --git a/tests/rest_api/test_tasks.py b/tests/rest_api/test_tasks.py index 9a082721..4ccda838 100644 --- a/tests/rest_api/test_tasks.py +++ b/tests/rest_api/test_tasks.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT +from copy import deepcopy from http import HTTPStatus from io import BytesIO from time import sleep @@ -32,6 +33,7 @@ def generate_image_files(count): return images +@pytest.mark.usefixtures('dontchangedb') class TestGetTasks: def _test_task_list_200(self, user, project_id, data, exclude_paths = '', **kwargs): response = get_method(user, f'projects/{project_id}/tasks', **kwargs) @@ -66,9 +68,6 @@ class TestGetTasks: assert response.status_code == HTTPStatus.OK assert any(_task['id'] == task['id'] for _task in response_data['results']) - # [sandbox] admin can see task data in project even he has no ownerships in this project - # [sandbox] business cannot see task data in project if he has no ownerships in this project - # [sandbox] user that has one of these ownerships: [Project:owner, Project:assignee] can see task data @pytest.mark.parametrize('project_id', [1]) @pytest.mark.parametrize('groups, is_staff, is_allow', [ ('admin', False, True), @@ -81,18 +80,14 @@ class TestGetTasks: self._test_users_to_see_task_list(project_id, tasks, users, is_staff, is_allow, is_project_staff) - # [sandbox] user that has one of these ownerships: [Owner, Assignee] can see task data @pytest.mark.parametrize('project_id, groups', [(1, 'user')]) - def test_task_assigneed_to_see_task(self, project_id, groups, users, tasks, find_users, is_task_staff): + def test_task_assigned_to_see_task(self, project_id, groups, users, tasks, find_users, is_task_staff): users = find_users(privilege=groups) tasks = list(filter(lambda x: x['project_id'] == project_id and x['assignee'], tasks)) assert len(tasks) self._test_assigned_users_to_see_task_data(tasks, users, is_task_staff) - # [organization] maintainer can see task data even if he has no ownerships in corresponding Project, Task - # [organization] supervisor cannot see task data if he has no ownerships in corresponding Project, Task - # [organization] worker (as role) that has one of these ownerships: [Project:owner, Project:assignee], can see task data @pytest.mark.parametrize('org, project_id', [({'id': 2, 'slug': 'org2'}, 2)]) @pytest.mark.parametrize('role, is_staff, is_allow', [ ('maintainer', False, True), @@ -105,7 +100,6 @@ class TestGetTasks: self._test_users_to_see_task_list(project_id, tasks, users, is_staff, is_allow, is_project_staff, org=org['slug']) - # [organization] worker (as role) that has one of these ownerships: [Owner, Assignee], can see task data @pytest.mark.parametrize('org, project_id, role', [ ({'id': 2, 'slug': 'org2'}, 2, 'worker') ]) @@ -117,7 +111,7 @@ class TestGetTasks: self._test_assigned_users_to_see_task_data(tasks, users, is_task_staff, org=org['slug']) -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPostTasks: def _test_create_task_201(self, user, spec, **kwargs): response = post_method(user, '/tasks', spec, **kwargs) @@ -127,31 +121,6 @@ class TestPostTasks: response = post_method(user, '/tasks', spec, **kwargs) assert response.status_code == HTTPStatus.FORBIDDEN - @staticmethod - def _wait_until_task_is_created(username, task_id): - url = f'tasks/{task_id}/status' - - while True: - response = get_method(username, url) - response_json = response.json() - if response_json['state'] == 'Finished' or response_json['state'] == 'Failed': - return response - sleep(1) - - def _test_create_task_with_images(self, username, spec, data, files): - response = post_method(username, '/tasks', spec) - assert response.status_code == HTTPStatus.CREATED - task_id = response.json()['id'] - - response = post_files_method(username, f'/tasks/{task_id}/data', data, files) - assert response.status_code == HTTPStatus.ACCEPTED - - response = self._wait_until_task_is_created(username, task_id) - response_json = response.json() - assert response_json['state'] == 'Finished' - - return task_id - def _test_users_to_create_task_in_project(self, project_id, users, is_staff, is_allow, is_project_staff, **kwargs): if is_staff: users = [user for user in users if is_project_staff(user['id'], project_id) ] @@ -171,9 +140,6 @@ class TestPostTasks: else: self._test_create_task_403(username, spec, **kwargs) - # [sandbox] admin can create task in project even he has no ownerships in this project - # [sandbox] business cannot create task in project if he has no ownerships in this project - # [sandbox] user that has one of these ownerships: [Project:owner, Project:assignee] and has less than 10 task can create task in project @pytest.mark.parametrize('project_id', [1]) @pytest.mark.parametrize('groups, is_staff, is_allow', [ ('admin', False, True), @@ -184,7 +150,6 @@ class TestPostTasks: users = find_users(privilege=groups) self._test_users_to_create_task_in_project(project_id, users, is_staff, is_allow, is_project_staff) - # [organization] worker cannot create task in project even he has no ownerships in this project @pytest.mark.parametrize('org, project_id', [({'id': 2, 'slug': 'org2'}, 2)]) @pytest.mark.parametrize('role, is_staff, is_allow', [ ('worker', False, False), @@ -193,32 +158,7 @@ class TestPostTasks: users = find_users(org=org['id'], role=role) self._test_users_to_create_task_in_project(project_id, users, is_staff, is_allow, is_project_staff, org=org['slug']) - def test_can_create_task_with_defined_start_and_stop_frames(self): - username = 'admin1' - task_spec = { - 'name': f'test {username} to create a task with defined start and stop frames', - "labels": [{ - "name": "car", - "color": "#ff00ff" - }], - } - - task_data = { - 'image_quality': 75, - 'start_frame': 2, - 'stop_frame': 5 - } - task_files = { - f'client_files[{i}]': image for i, image in enumerate(generate_image_files(7)) - } - - task_id = self._test_create_task_with_images(username, task_spec, task_data, task_files) - - # check task size - response = get_method(username, f'tasks/{task_id}') - response_json = response.json() - assert response_json['size'] == 4 - +@pytest.mark.usefixtures('dontchangedb') class TestGetData: _USERNAME = 'user1' @@ -232,9 +172,9 @@ class TestGetData: assert response.status_code == HTTPStatus.OK assert response.headers['Content-Type'] == content_type -@pytest.mark.usefixtures("restore") +@pytest.mark.usefixtures('changedb') class TestPatchTaskAnnotations: - def _test_check_respone(self, is_allow, response, data=None): + def _test_check_response(self, is_allow, response, data=None): if is_allow: assert response.status_code == HTTPStatus.OK assert DeepDiff(data, response.json(), @@ -245,7 +185,7 @@ class TestPatchTaskAnnotations: @pytest.fixture(scope='class') def request_data(self, annotations): def get_data(tid): - data = annotations['task'][str(tid)].copy() + data = deepcopy(annotations['task'][str(tid)]) data['shapes'][0].update({'points': [2.0, 3.0, 4.0, 5.0, 6.0, 7.0]}) data['version'] += 1 return data @@ -269,7 +209,7 @@ class TestPatchTaskAnnotations: response = patch_method(username, f'tasks/{tid}/annotations', data, org_id=org, action='update') - self._test_check_respone(is_allow, response, data) + self._test_check_response(is_allow, response, data) @pytest.mark.parametrize('org', [2]) @pytest.mark.parametrize('role, task_staff, is_allow', [ @@ -288,10 +228,26 @@ class TestPatchTaskAnnotations: response = patch_method(username, f'tasks/{tid}/annotations', data, org_id=org, action='update') - self._test_check_respone(is_allow, response, data) + self._test_check_response(is_allow, response, data) + +@pytest.mark.usefixtures('dontchangedb') +class TestGetTaskDataset: + def _test_export_project(self, username, tid, **kwargs): + response = get_method(username, f'tasks/{tid}/dataset', **kwargs) + assert response.status_code == HTTPStatus.ACCEPTED + + response = get_method(username, f'tasks/{tid}/dataset', **kwargs) + assert response.status_code == HTTPStatus.CREATED + + response = get_method(username, f'tasks/{tid}/dataset', action='download', **kwargs) + assert response.status_code == HTTPStatus.OK + + def test_admin_can_export_task_dataset(self, tasks_with_shapes): + task = tasks_with_shapes[0] + self._test_export_project('admin1', task['id'], format='CVAT for images 1.1') -@pytest.mark.usefixtures("restore") -class TestExportDatasetTask: +@pytest.mark.usefixtures("changedb") +class TestPostTaskData: @staticmethod def _wait_until_task_is_created(username, task_id): url = f'tasks/{task_id}/status' diff --git a/tests/rest_api/test_users.py b/tests/rest_api/test_users.py index 7fbccd4d..8caf2240 100644 --- a/tests/rest_api/test_users.py +++ b/tests/rest_api/test_users.py @@ -3,10 +3,14 @@ # SPDX-License-Identifier: MIT from http import HTTPStatus + +import pytest from deepdiff import DeepDiff -from .utils.config import get_method +from rest_api.utils.config import get_method + +@pytest.mark.usefixtures('dontchangedb') class TestGetUsers: def _test_can_see(self, user, data, endpoint='users', exclude_paths='', **kwargs): response = get_method(user, endpoint, **kwargs) diff --git a/tests/rest_api/utils/__init__.py b/tests/rest_api/utils/__init__.py new file mode 100644 index 00000000..1fd19bae --- /dev/null +++ b/tests/rest_api/utils/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/tests/rest_api/utils/config.py b/tests/rest_api/utils/config.py index 7183a7b8..a114a7d6 100644 --- a/tests/rest_api/utils/config.py +++ b/tests/rest_api/utils/config.py @@ -5,8 +5,8 @@ import os.path as osp import requests -ROOT_DIR = osp.dirname(__file__) -ASSETS_DIR = osp.abspath(osp.join(ROOT_DIR, '..', 'assets')) +ROOT_DIR = __file__[:__file__.rfind(osp.join("utils", ""))] +ASSETS_DIR = osp.abspath(osp.join(ROOT_DIR, 'assets')) # Suppress the warning from Bandit about hardcoded passwords USER_PASS = '!Q@W#E$R' # nosec BASE_URL = 'http://localhost:8080/'